/*******************************************************************************
 * Copyright 2011 Mile Orešković
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package com.mileoresko.hierarchism;

import java.util.Hashtable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mileoresko.hierarchism.exceptions.HSMCommonTransitionDefinitionException;
import com.mileoresko.hierarchism.exceptions.HSMDuplicateTransitionException;
import com.mileoresko.hierarchism.exceptions.HSMInternalTransitionDefinitionException;
import com.mileoresko.hierarchism.exceptions.HSMMissingInitialTransitionException;
import com.mileoresko.hierarchism.exceptions.HSMPseudoChoiceDefinitionException;

public abstract class HierarchicalStateMachine {

    private static final String LOG_HSM_ALREADY_RUNNING_IGNORING_START = "HSM already running! Ignoring start request.";
    private static final String LOG_RESUMING_HSM = "Resuming HSM ...";
    private static final String LOG_STARTING_HSM = "Starting HSM ...";
    private static final String LOG_HSM_NOT_DEFINED_IGNORING_START = "HSM not defined (created)! Ignoring start request!";
    private static final String LOG_COMMON_TRANSITION = "CommonT: {}";
    private static final String LOG_PSEUDO_CHOICE_START = "PseudoT: {}";
    private static final String LOG_PSEUDO_CHOICE_END = "PseudoT-done: {} | EndState: {}";
    private static final String LOG_INTERNAL_TRANSITION = "InternalT: {}";
    private static final String LOG_GUARD_REJECTED_TRANSITION = "Guard rejected transition: {}";
    private static final String LOG_HSM_NOT_RUNNING_IGNORING_INPUT = "HSM not running! Ignoring input!";
    private static final String LOG_HSM_PAUSED = "Resumable HSM paused.";
    private static final String LOG_HSM_STOPPED = "HSM stopped.";
    private static final String LOG_HSM_NOT_DEFINED_IGNORING_HALT = "HSM not defined! Ignoring halt request.";
    private static final String LOG_HSM_ALREADY_OFF_IGNORING_HALT = "HSM already off! Ignoring halt request.";
    private static final String LOG_HSM_ALREADY_PAUSED_IGNORING_HALT = "HSM already paused! Ignoring halt request.";

    private static final int STATUS_UNDEFINED = 0;
    private static final int STATUS_OFF = 1;
    private static final int STATUS_ON = 2;
    private static final int STATUS_PAUSED = 3;

    private final Hashtable transitions;
    private Hashtable eventMappings;
    private final String label;
    private boolean isResumable;

    private HSMBareState currentState;
    private int hsmStatus;

    private final Logger logger;

    protected HierarchicalStateMachine(final String label) {
        this.label = label;
        transitions = new Hashtable();
        isResumable = false;
        hsmStatus = STATUS_UNDEFINED;
        logger = LoggerFactory.getLogger(label);
    }

    protected void addChoicePseudostate(final HSMBareState state, final HSMEvent event,
            final HSMChoicePseudostateAdapter transitionAdapter, boolean loggable)
            throws HSMPseudoChoiceDefinitionException, HSMDuplicateTransitionException {
        if (state == null || event == null || transitionAdapter == null) {
            throw new HSMPseudoChoiceDefinitionException(state, event, transitionAdapter);
        }
        addTransition(state, event, null, transitionAdapter, loggable);
    }

    protected void addChoicePseudostate(final HSMBareState state, final HSMEvent event,
            final HSMChoicePseudostateAdapter transitionAdapter) throws HSMPseudoChoiceDefinitionException,
            HSMDuplicateTransitionException {
        addChoicePseudostate(state, event, transitionAdapter, true);
    }

    protected void addCommonTransition(final HSMBareState sourceState, final HSMEvent event,
            final HSMBareState destinationState, final HSMTransitionListener transitionListener, boolean loggable)
            throws HSMDuplicateTransitionException, HSMCommonTransitionDefinitionException {
        if (sourceState == null || event == null || destinationState == null) {
            throw new HSMCommonTransitionDefinitionException(sourceState, event, destinationState);
        }
        addTransition(sourceState, event, destinationState, transitionListener, loggable);
    }

    protected void addCommonTransition(final HSMBareState sourceState, final HSMEvent event,
            final HSMBareState destinationState, final HSMTransitionListener transitionListener)
            throws HSMDuplicateTransitionException, HSMCommonTransitionDefinitionException {
        addCommonTransition(sourceState, event, destinationState, transitionListener, true);
    }

    protected void addInternalTransition(final HSMBareState state, final HSMEvent event,
            final HSMInternalTransitionAdapter transitionAdapter, boolean loggable)
            throws HSMInternalTransitionDefinitionException, HSMDuplicateTransitionException {
        if (state == null || event == null || transitionAdapter == null) {
            throw new HSMInternalTransitionDefinitionException(state, event, transitionAdapter);
        }
        addTransition(state, event, state, transitionAdapter, loggable);
    }

    protected void addInternalTransition(final HSMBareState state, final HSMEvent event,
            final HSMInternalTransitionAdapter transitionAdapter) throws HSMInternalTransitionDefinitionException,
            HSMDuplicateTransitionException {
        addInternalTransition(state, event, transitionAdapter, true);
    }

    protected void create() throws HSMMissingInitialTransitionException, HSMDuplicateTransitionException,
            HSMInternalTransitionDefinitionException, HSMPseudoChoiceDefinitionException,
            HSMCommonTransitionDefinitionException {
        if (hsmStatus == STATUS_UNDEFINED) {
            if (getInitialState() == null) {
                throw new HSMMissingInitialTransitionException(label);
            }
            defineTransitions();
            onCreate();
            hsmStatus = STATUS_OFF;
        }
    }

    protected abstract void defineTransitions() throws HSMDuplicateTransitionException,
            HSMInternalTransitionDefinitionException, HSMPseudoChoiceDefinitionException,
            HSMCommonTransitionDefinitionException;

    protected abstract HSMState getInitialState();

    protected void onCreate() {

    }

    protected void onPause(Object arg) {

    }

    protected void onResume(Object arg) {

    }

    protected void onStart(Object arg) {

    }

    protected void onStop(Object arg) {

    }

    public void addEventMapping(final HSMEvent originalEvent, final HSMEvent mappedEvent) {
        if (eventMappings == null) {
            eventMappings = new Hashtable();
        }
        eventMappings.put(originalEvent, mappedEvent);
    }

    public HSMBareState getCurrentState() {
        return currentState;
    }

    public void halt(final Object arg) {
        switch (hsmStatus) {
        case STATUS_ON:
            currentState.exitActionInternal(arg);
            break;
        case STATUS_PAUSED:
            if (logger.isDebugEnabled()) {
                logger.debug(LOG_HSM_ALREADY_PAUSED_IGNORING_HALT);
            }
            return;
        case STATUS_OFF:
            if (logger.isDebugEnabled()) {
                logger.debug(LOG_HSM_ALREADY_OFF_IGNORING_HALT);
            }
            return;
        case STATUS_UNDEFINED:
            if (logger.isDebugEnabled()) {
                logger.debug(LOG_HSM_NOT_DEFINED_IGNORING_HALT);
            }
            return;
        }
        hsmStatus = STATUS_PAUSED;
        onPause(arg);
        if (!isResumable) {
            hsmStatus = STATUS_OFF;
            onStop(arg);
            if (logger.isDebugEnabled()) {
                logger.debug(LOG_HSM_STOPPED);
            }
        } else {
            if (logger.isDebugEnabled()) {
                logger.debug(LOG_HSM_PAUSED);
            }
        }
    }

    public boolean isDown() {
        return hsmStatus == STATUS_OFF || hsmStatus == STATUS_UNDEFINED;
    }

    public boolean isPaused() {
        return hsmStatus == STATUS_PAUSED;
    }

    public boolean isRunning() {
        return hsmStatus == STATUS_ON;
    }

    public synchronized void pushEvent(HSMEvent event, final Object arg) {
        if (!isRunning()) {
            if (logger.isDebugEnabled()) {
                logger.debug(LOG_HSM_NOT_RUNNING_IGNORING_INPUT);
            }
            return;
        }

        event = getMappedEvent(event);

        currentState.dispatchEvent(event, arg);

        final HSMTransition transition = getDefinedTransition(new HSMTransitionIdentifier(currentState, event));
        if (transition == null) {
            return;
        }

        switch (transition.getTransitionType()) {
        case HSMTransition.TRANSITION_TYPE_INTERNAL:
            processInternalTransition(transition, arg);
            return;
        case HSMTransition.TRANSITION_TYPE_PSEUDOCHOICE:
            processChoicePseudostate(transition, arg);
            break;
        case HSMTransition.TRANSITION_TYPE_COMMON:
            processCommonTransition(transition, arg);
            break;
        }

        currentState.dispatchEvent(event, arg);
    }

    public void setResumable(final boolean resumable) {
        isResumable = resumable;
    }

    public void start(final Object arg) {
        switch (hsmStatus) {
        case STATUS_UNDEFINED:
            if (logger.isDebugEnabled()) {
                logger.debug(LOG_HSM_NOT_DEFINED_IGNORING_START);
            }
            return;
        case STATUS_OFF:
            if (logger.isDebugEnabled()) {
                logger.debug(LOG_STARTING_HSM);
            }
            onStart(arg);
            currentState = getInitialState();
        case STATUS_PAUSED:
            if (logger.isDebugEnabled()) {
                logger.debug(LOG_RESUMING_HSM);
            }
            onResume(arg);
            currentState.entryActionInternal(arg);
            break;
        case STATUS_ON:
            if (logger.isDebugEnabled()) {
                logger.debug(LOG_HSM_ALREADY_RUNNING_IGNORING_START);
            }
            break;
        }
        hsmStatus = STATUS_ON;
    }

    public String toString() {
        StringBuffer builder = new StringBuffer();
        switch (hsmStatus) {
        case STATUS_ON:
            return builder.append("[").append(label).append("@").append(currentState).append("]").toString();
        case STATUS_PAUSED:
            return builder.append("[").append(label).append("::PAUSED@").append(currentState).append("]").toString();
        default:
            return builder.append("[").append(label).append("]").toString();
        }
    }

    private void addTransition(final HSMBareState sourceState, final HSMEvent event,
            final HSMBareState destinationState, final HSMTransitionListener transitionListener, boolean loggable)
            throws HSMDuplicateTransitionException {
        final HSMTransition transition = new HSMTransition(sourceState, event, destinationState, transitionListener,
                loggable);
        final HSMTransitionIdentifier id = transition.getTransitionIdentifier();
        if (transitions.containsKey(id)) {
            throw new HSMDuplicateTransitionException(toString(), getDefinedTransition(id), transition);
        }
        transitions.put(id, transition);
    }

    private HSMTransition getDefinedTransition(final HSMTransitionIdentifier id) {
        return (HSMTransition) transitions.get(id);
    }

    private HSMEvent getMappedEvent(final HSMEvent originalEvent) {
        if (eventMappings != null) {
            final HSMEvent mappedEvent = (HSMEvent) eventMappings.get(originalEvent);
            if (mappedEvent != null) {
                return mappedEvent;
            } else {
                return originalEvent;
            }
        } else {
            return originalEvent;
        }
    }

    private void processChoicePseudostate(final HSMTransition transition, final Object arg) {
        if (logger.isDebugEnabled() && transition.isLoggable()) {
            logger.debug(LOG_PSEUDO_CHOICE_START, transition);
        }
        currentState.exitActionInternal(arg);
        currentState = ((HSMChoicePseudostateAdapter) transition.getTransitionListener()).choicePseudostateTransition(arg);
        currentState.entryActionInternal(arg);
        if (currentState.isTerminal()) {
            hsmStatus = STATUS_OFF;
        }
        if (logger.isDebugEnabled() && transition.isLoggable()) {
            logger.debug(LOG_PSEUDO_CHOICE_END, transition, currentState);
        }
    }

    private void processCommonTransition(final HSMTransition transition, final Object arg) {
        final HSMTransitionListener transitionListener = transition.getTransitionListener();
        if (transitionListener != null) {
            if (!transitionListener.transitionAllowed(arg)) {
                if (logger.isDebugEnabled() && transition.isLoggable()) {
                    logger.debug(LOG_GUARD_REJECTED_TRANSITION, transition);
                }
                return;
            }
        }
        if (logger.isDebugEnabled() && transition.isLoggable()) {
            logger.debug(LOG_COMMON_TRANSITION, transition);
        }
        currentState.exitActionInternal(arg);
        if (transitionListener != null) {
            transitionListener.transitionAction(arg);
        }
        currentState = transition.getDestinationState();
        transition.getDestinationState().entryActionInternal(arg);
        if (currentState.isTerminal()) {
            hsmStatus = STATUS_OFF;
        }
    }

    private void processInternalTransition(final HSMTransition transition, final Object arg) {
        if (logger.isDebugEnabled() && transition.isLoggable()) {
            logger.debug(LOG_INTERNAL_TRANSITION, transition);
        }
        transition.getInternalTransitionAdapter().transitionAction(arg);
    }
}
